---
title: 'React设计原理'
description: '本文基于React18，介绍了当前主流前端框架的实现原理，以及React的设计理念和实现原理。'
date: '2023-03-15'
hot: true
published: true
toc: true
---

## 前端框架概览

### 初识前端框架

> 鲁迅曾经说过，现代所有前端框架的实现原理可以概括为一个公式：
> UI = f(state)

关于公式：

- state 代表试图所用的数据状态
- f 代表框架的运行机制
- UI 代表特定环境的视图

#### 描述 UI

前端领域描述 UI 有两种主流方案

1. Meta 家的 JSX
2. 由后端发展而来的模版语言

JSX 作为 ECMAsript 的增强语法糖，在书写 Html 元素相关属性和操作时，需要关注一些差异性书写方式，如`class`要书写为`className`。其灵活性带来的缺陷也是模版语言的优势：无法提前做编译静态分析

模版语言是使用如`Mustache`等解析库，将模版插值进行替换的一种描述前端 UI 的语言。需要关注一些特定实现的插值书写方式,如`Vue`中的`v-bind`。因为模版变化有很高预测性，可以静态分析优化性能。其最大缺陷就是**模版与上下文割裂**，灵活性也受到框架提供的插值 Api 的影响。

#### 组织 UI 与逻辑

1. Vue2.x 的定制化发布订阅模式
2. React Hooks、Vue Composition、Svelte、SolidJS 的副作用模式

定制化和副作用模式根本逻辑是相同的。`逻辑或元素`读取值时收集当前读取的对象，然后变化发生时通知收集的对象。

设计思想有些许不同：

Vue2.x 中的`Dep`和`Watcher`,相互引用记录对方，何时收集何时通知，嵌套和连环关联如何管理，并没有很好的心智模型。

副作用模式中我们只用关心**自变量**和**因变量**，自变量变化导致因变量变化。
考虑如下公式：

〉2x +1 = y

x 的变化会导致 y 的变化，因此 x 被称为自变量，y 被称为因变量。

自变量发生变化，会导致**使用或依赖**它的因变量变化。在前端框架中，因变量有两种：

1. 无副作用因变量；
2. 有副作用因变量；
   那么**副作用**又是什么呢？

**副作用**时函数式编程中提出的概念。指会**函数执行过程中对外部环境的影响**。如果一个函数没有副作用，那么这个函数为**纯函数**：

1. 相同输入始终获得相同的输出
2. 不过修改程序状态或者引起副作用
   举个栗子：

```javascript
// 纯函数
function sum(n) {
  return 2n
}
// 非纯函数
function sumRandom(x) {
  return x + Math.random()
}

// 非纯函数
function calc(n) {
  document.title = n // 副作用
  localStorage.set('calc', n) // 副作用
  return 2n
}
```

除了修改函数外部变量外，调用 DOM API、I/O 操作、控制台打印信息等“函数运行过程中，外部可观察的变化”都属于副作用。

因为因变量不需要赋值，其值由自变量变化而自动变化。因此无副作用的因变量应定义为纯函数。

举个栗子：

```javascript
// vue composition 无副作用因变量
const x = ref(0)
const val = computed(() => x.value + 1)
// vue composition 有副作用因变量
watchEffect(() => (document.title = x.value))
// react hooks 无副作用因变量
const [x, setX] = useState(0)
const y = useMemo(() => x + 1, [x])
// react hooks 有副作用因变量
useEffect(() => (document.title = x), [x])
```

那么如何组织逻辑与 UI 呢？

当自变量发生变化，如果 UI 中使用了因变量，那么 UI 中的内容也会发生变化。

举个栗子：

```jsx
// 自变量引起变化
function Counter() {
  const [num, setNum] = useState(0)
  return <p onClick={() => setNum(num + 1)}>点击次数：{num}</p>
}
```

当我们点击了文本，点击次数会让自变量加 1，并即时更新到 UI。**逻辑中的自变量变化会导致 UI 变化**

```jsx
// 无副作用因变量引起变化
function Counter() {
  const [num, setNum] = useState(0)
  const fixed = useMemo(() => num.toFixed(2), [num])
  return <p onClick={() => setNum(num + 1)}>点击次数：{fixed}</p>
}
```

当我们点击文本，点击次数让因变量发生了变化，视图也即时更新了。**逻辑中的变化，导致无副作用因变量发生变化，进而导致 UI 变化**

```jsx
function Counter() {
  const [num, setNum] = useState(0)
  useEffect(() => {
    console.log('点击次数：', num)
  }, [num])
  return <p onClick={() => setNum(num + 1)}>点击次数：{num}</p>
}
```

当我们点击一次文本，视图变化的同时，控制台输出了`点击次数：1`，并且每次点击视图和控制台输出的变化是一致的。。**逻辑中的变化，导致有副作用因变量发生变化，进而导致副作用**

#### 如何在组件之前传输数据

加入我们有如下函数组件：

```jsx
function Counter({ value }) {
  return <p>{{ value }}</p>
}
```

在 React 中有两种方式： 1.通过`props`传递，如`<Counter value={10} />`，在`Counter`组件中，函数参数中可以解构出传递的`value`,组件会渲染出**值为 10 的文本标签** 2.通过`store`传递，`store`的本质也是自变量。它能实现跨层级传递。

> 在`React`中，有多种`store`方案，但`store`的定义都需要遵循三个步骤：
>
> 1.在顶级组件中调用`React.createContext`创建`context`
>
> 2.在顶级组件中定义`context.provider`
>
> 3.在子孙组件中通过`useContext`消费顶级组件传过来的自变量

#### 框架的分类

回顾鲁迅说的 UI 公式：UI=f(state)

`state`的本质就是自变量，自变量变化导致`UI`变化，单框架中此处的`UI`并非一定就是宿主环境的真实 UI，也许是诸如`vNode`之类的对宿主环境的描述。

所以前端框架的工作原理分为两步：

1. 根据公示计算出最新的对宿主环境的 UI 描述
2. 根据新的 UI 描述和变化，执行真实宿主环境的更新视图 API
   在浏览器环境，更新视图的 API，主要是 DOM 提供的 API，有`Node.appendChild`、`Node.removeChild`等等

通过对描述和更新粒度的归类，前端框架可以分为三类：

1. 应用级框架，如`React`，状态变化会导致应用从顶至下对比 UI 的描述变化
2. 组件级框架，如`Vue`，状态变化会导致组件对比 UI 的描述变化
3. 元素级框架，如`Sevlet`和`Solid`，状态变化会导致元素描述的变化

#### React 中的自变量与因变量

将`React Hooks`分类如下：

- `useState`: 定义组件内部的自变量
- `useReducer`: `useState`本质是内置`reducer`的`useReducer`。如果将`useReducer`看作**借鉴 Redux 理念的 useState**，也相当于组件内部定义的自变量。
- `useContext`: `React`中`store`的实现，用于夸层级将其他组件的自变量传递给当前组件。
- `useMemo`: 采用缓存的方式定义组件内部无副作用因变量
- `useCallback`: 采用缓存的方式定义组件内部无副作用因变量，缓存值为**函数**
- `useEffect`和`useLayoutEffect`: 定义组件内部有副作用因变量
- `useRef`: 自定义执行有副作用的操作

### 主流前端框架使用的技术

#### 关于细粒度更新

在`React`中定义因变量需要显示指明**因变量依赖的自变量**，然而在`Vue`、`Mobx`中并不需要显示的指明。

举个栗子感受一下：

```javascript
// React Hook
const y = useMemo(() => x * 2, [x])
// Vue Composition
const y = computed(() => x.value * 2)
// Mobx
const y = computed(() => x.data * 2)
```

可以发现`Vue`和`Mobx`中使用的能够自动追踪依赖的技术，被称为**细粒度更新**，它同时也是许多前端建立**自变量变化到 UI 变化**的底层原理
由`KnockoutJS`在 2010 年初采用，并称为**响应式更新**

---

下面实现一个**细粒度更新**。**下列的`React Hook` API 均为`Fake`API。**

基础框架：

```javascript
function useState(value) {
  const getter = () => value // 与React不同，这里并不是直接返回value，用于后续订阅副作用
  const setter = (newValue) => (value = newValue)

  return [getter, setter]
}
```

接下来实现**有副作用因变量**———fake`useEffect`,期望的行为是：

1. `useEffect`执行后，回调函数立即执行
2. 依赖的自变量变化后，毁掉函数立即执行
3. 不需要显示指明依赖

建立`useState`和`useEffect`的发布订阅关系：

1. 在`useEffect`回调中执行`useState`的`getter`时，该`effect`会订阅“该 state 的变化”，收集依赖
2. `useState`的`setter`在执行时，会向所有订阅了该`state`变化的`effect`发布通知，通知依赖
   ![effect和state的关系](https://static.wissbell.com/wissbell/photos/fake-relative.png?imageMogr2/format/webp)
   定义`effect`

```javascript
const effect = {
  execute, // 用于执行useEffect回调函数
  deps: new Set(), // 保存该useEffect依赖的state对应subs的集合
}
```

通过遍历`state.subs`，可以找到所有“订阅该 state 变化的 effect”。通过遍历`effct.deps`,可以找到所有“该 effect 依赖的 state.subs”.

实现如下：

```javascript
function useEffect(callback) {
  const execute = () => {
    cleanup(effect) // 重置依赖
    effectStack.push(effect)

    try {
      callback()
    } finally {
      effectStack.pop() // 执行完毕，effect出栈
    }

    const effect = {
      execute,
      deps: new Set(),
    }

    execute() // 立刻执行一次，建立发布订阅关系
  }
}
```

关于细节：

1. `callback`执行前调用`cleanup`清除所有与该`effect`相关的订阅发布关系（重新建立订阅发布关系）
2. `callback`执行前将当前`effect`保存在`effectStack`栈顶，方便处理当前所处的`effect`上下文
3. 最后，在`useEffect`执行后，会执行`execute`，建立订阅发布关系。自动收集依赖才能生效

关于为什么要执行`cleanup`?

举个栗子：

```javascript
const [name1, setName1] = useState('k')
const [name2, setName2] = useState('w')
const [showAll, setShowAll] = useState(true)

const display = useMemo(() => {
  if (!showAll) {
    return name1()
  }
  return `${name1()}和${name2()}`
})

useEffect(() => {
  console.log(display())
})
```

如果执行了`setShowAll(false)`，那么代码逻辑就不会走到`name2()`,这样一来`display`和`name2`就没有发布订阅关系。
只有当`stShowAll(true)`被执行后，`display`才会重新依赖`name1`和`name2`。

修改`useState`，完成订阅与发布的逻辑

```javascript
function useState(value) {
  const subs = new Set() // Set保证了不会添加重复数据

  const getter = () => {
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
      subscribe(effect, subs) // 订阅
    }
    return value
  }

  const setter = (newValue) => {
    value = newValue

    for (const effect of [...subs]) {
      effect.execute() //批量发布
    }
  }

  return [getter, setter]
}

function subscribe(effect, subs) {
  subs.add(effect)
  effect.deps.add(subs)
}
```

实现`useState`和`useEffect`之后，可以在此基础实现`useMemo`:

```javascript
function useMemo(callback){
  const [s, set] = useState()
  useEffect(()=>set(callback())
  return s
}
```

关于细粒度更新的实现完整代码如下：

```javascript
 const effectStack = []

 function subscribe(effect, subs) {
  subs.add(effect)
  effet.deps.add(subs)
 }

 function cleanup(effect) {
  for (const subs of effect.deps){
    subs.delete(effect)
  }
  effect.deps.clear()
 }

function useState(value){
  const subs = new Set() // Set保证了不会添加重复数据

  const getter =() =>{
    const effect = effectStack[effectStack.length -1]
    if(effect){
      subscribe(effect,subs) // 订阅
    }
    return value
  }

  const setter = (newValue) =>{
    value = newValue

    for(const effect of [...subs]) {
      effect.execute() //批量发布
    }
  }

  return [getter, setter]
}

function useEffect(callback) {
 const execute = () => {
  cleanup(effect) // 重置依赖
  effectStack.push(effect)

  try{
    callback()
  } finally {
    effectStack.pop() // 执行完毕，effect出栈
  }

  const effect = {
    execute,
    deps: new Set()
  }

  execute() // 立刻执行一次，建立发布订阅关系
 }
}

function useMemo(callback){
  const [s, set] = useState()
  useEffect(()=>set(callback())
  return s
}
```

上述代码实现了**细粒度更新**版本的 React Hooks。相比原版 API。它有两个优点：

1. 无需显式指明依赖
2. 由于可以自动追踪依赖，因此不受**React Hooks 不能在条件语句中声明的限制**
   既然如此，那么为什么`React Hooks`不采用细粒度更新呢？回顾框架分类，`React`属于应用级框架，
   **关注自变量与应用的关系**，其更新粒度不需要很细。因此无须使用细粒度更新。作为代价，`React Hooks`在使用上则会有上述两个优点相对应的缺点。

其次我们的 Fake API 与 React API 还有一个区别。`useState`第一个参数上一个函数，因此取值的时候需要调用。

Solid.js 使用了我们实现的方式，`Vue2/3`中分别使用了对象存取描述符和 Proxy 封装了 getValue，隐藏了函数这一细节。

#### AOT

现代前端框架都需要**编译**这一步骤，编译可以选择两个时机执行：

- 构建时，AOT
- 代码在宿主环境执行时，JIT

AOT 能在构建过程中进行分析，优化得到性能提升。JIT 提供快速的开发体验，但运行时会增加编译器代码

虽然 React 属于 AOT，但由于 JSX 是 ES 的语法糖，ES 语句的高度灵活性，使其很难进行静态分析从而获得如 Vue 的静态分析优化。React 也尝试过使用新的 AOT 实现来优化 React 性能。`prepack`是一个可以自动生成等效于`useMemo`和`useCallback`代码的编译器。但项目已于 2019 年暂停。

#### Virtual DOM

前端框架可以从 AOT 中获得许多益处，AOT 可以减少根据自变量变化计算出 UI 变化的作用。Virtual Dom 作为一种 DOM UI 描述的主流技术，其原理概括为两个步骤：

1. 将元素描述的 UI 转化为 VDOM 描述的 UI
2. 对比前后 VDOM 描述的 UI，计算出变化的部分。

在 Vue 中：

1. render 函数执行后返回 VNode 描述的 UI，称为 render
2. 将变化前后的 VNode 进行比较，计算出变化部分，称为 patch

在 React 中：

1. createElement 方法执行后返回的 React Element 描述的 UI
2. 将 React Element 描述的 UI 与变化前 FiberNode 描述的 UI 进行比较，计算出变化部分，同时生成本次更新 FiberNode 描述的 UI

VDOM 的优点：

- 相较于 DOM 体积更小
- 相较于 AOT 更强大的描述能力
- 多平台渲染的抽象能力

### 前端框架的实现原理

简单介绍一下元素级、组件级、应用级框架的实现原理。

#### Svelte

```html
<h1 on:click="{count++}">{count}</h1>

<script>
  let count = 0
</script>
```

组件编译后会生成：

- c create 函数
- m mount 函数
- d detach 函数
- p update 函数
  编译后的代码如下：

```javascript
c() {
 h1 = element("h1")
 t = text(/*count*/ ctx[0])
}
m(target,anchor) {
  insert(target, h1, anchor)
  append(h1, t)
  dispose = listen(h1, "click", /*update*/ctx[1]) // 绑定⌚️
}
p(new_ctx, [dirty]) {
  ctx = new_ctx
  if(dirty && /*count*/ 1) set_data(t0, /*count*/ ctx[0])
}
d(detaching) {
  if(detaching) detach(h1)
  despose() // 解绑事件
}
```

1. click 执行回调函数 update
2. update 内调用`$$invalidate`方法，更新 ctx 中的 ctx，标记 count 为 dirt，调度更新。
3. 执行 p 方法，方法内 dirty 的项（count）对应的 if 语句会执行具体的 DOM 操作

#### Vue3

```html
<script setup>
  import { ref } from 'vue'
  let count = ref(0)
</script>

<template>
  <h1 @click="count++">{{count}}</h1>
  <p>不会变化</p>
</template>
```

Vue3 会为每个组件都建立自变量与 UI 的 watchEffect 关系，在 watchEffect 首次及后续依赖变化后执行如下步骤：

1. 调用组件的 render 函数，生成组件对应 VNode
2. render 函数返回本次更新的 VNode，与上一次更新的 VNode 同时传入 patch 方法。执行 VDOM 相关操作，找到本次自变量变化导致的元素变化，并执行最终的 DOM 操作
   当点击事件导致 count 变化，Vue3 将执行订阅 count 变化的 effect 回调函数，重复上述两个步骤，完成 UI 渲染。总结为：

- 自变量变化，对应 effect 回调函数执行
- effect 回调函数执行，对应组件 UI 更新

Vue3 是如何通过 AOT 获得静态优化的？

组件模版进行 patch 方法时，VNode 会一一对比元素：

- h1 与 h1 比较
- p 与 p 比较
  通过观察发现，只有 h1 是可变的，其于的比较是无意义的。因此编译后的 render 函数：

```javascript
(_ctx, _cache) => {
  return (_openBlock(), null, null,[
    _createElementVNode(h1, null, _toDisplayString(_ctx.count), 1 /* TEXT */)
    _createElementVNode(h1, null,"不会变化")
  ])
}
```

h1 所对应的\_createElementVNode 函数第 4 个参数为 1，该参数被称为`PatchFlags`，代表 VNode 是可变的。如：

- 1 代表可变 textContent
- 2 代表 class 可变
  render 函数执行后，标记为 patchFlag 的可变部分被提取到 dynamicChildren 中。

当 patch 执行时，只需要遍历 dynamicChildren，而不需要遍历树结构中的所有 children。减少运行 patch 时对比 VDOM 的节点，实际的 patch 可能更复杂，比如需要考虑：

- v-if、v-for 等条件语句生成的 VNode 不稳定
- Fragment 造成的子节点不稳定

#### React

作为应用级框架，React 实现原理很简单：

> react 实现原理
> 事件触发-> reconcile -> commit
> 按照步骤概括为：

1. 触发事件，该变自变量，开启更新流程
2. 执行 VDOM 相关操作，在 React 中称为 reconcile
3. 根据步骤 2 计算出的需要变化的 UI 执行对应的 UI 操作，在 React 中称为 commit
   React 被称为应用级框架的原因在于——其每次更新流程都是从应用的根节点开始，遍历整个应用，对比其他框架：

- Vue3 的更新流程开始于组件
- Svelte 的更新流程开始于元素

基于这样的心智模型，React 无须确定哪个自变量发生了变化，因为任何自变量的变化都会开启一次从头到位的遍历应用更新流程。因此 React 不需要细粒度更新和 AOT

因为每次更新都遍历应用，因此 React 性能优化的任务交到了开发者手中。而 Vue 中不存在如`React.memo`之类的 API，是因为组件级框架的定位和其模版 AOT 优化以及减少了大部分无意义的遍历对比过程。

## React 理念

### 问题与解决思路

在 React 官网中，“React 哲学”一节提到，React 的理念是：

> 我们认为，React 是用 Javascript 构建快速响应的大型 Web 应用的首选方式。它在 Facebook 和 Instagram 上表现优秀
> 制约“快速响应”有两类场景：

1. 当执行大量计算的操作或设备性能不足时，页面掉帧，导致卡顿，为 CPU 瓶颈
2. 进行 I/O 操作后，需要等待数据返回才能继续操作，等待的过程导致不能快速响应，为 I/O 瓶颈

为什么一段复杂 JS 代码会导致页面卡顿，这**与浏览器渲染与事件循环有关**

#### 浏览器渲染

#### 事件循环

#### CPU 瓶颈

#### I/O 瓶颈

### React 架构演进

#### 新旧架构介绍

#### 主打特性的迭代

#### 渐进升级策略的迭代

### Fiber 架构

### 调试 React 源码

## render 阶段

## commit 阶段

## schedule 阶段

## 状态更新流程

## reconcile 流程

## FC 与 Hooks 实现
